<?php

namespace ASW\Communication;

use ASW\Utility\ExecuteResult;
use Throwable;
use Workerman\Timer;

/**
 * 事件通道客户端
 */
class EventChannelClient
{
    /**
     * @var ApiClient 客户端
     */
    private ApiClient $client;

    /**
     * @var callable $onConnected 连接成功时的回调
     */
    private $onConnected = null;

    /**
     * @var callable $onSubscribeHandler 发送订阅请求时的回调
     */
    private $onSubscribeHandler = null;

    /**
     * @var callable $onUnSubscribeHandler 发送取消订阅请求时的回调
     */
    private $onUnSubscribeHandler = null;

    /**
     * @var callable $onPublishResponseHandler 发送发布请求返回时的回调
     */
    private $onPublishResponseHandler = null;

    /**
     * @var callable $onSubscribeResponseHandler 发送订阅请求返回时的回调
     */
    private $onSubscribeResponseHandler = null;

    /**
     * @var callable $onUnSubscribeResponseHandler 发送取消订阅请求返回时的回调
     */
    private $onUnSubscribeResponseHandler = null;

    private array $eventHandlerDic = [];


    public function __construct(string $serverHost = '127.0.0.1', int $serverPort = 2018)
    {
        $this->client = new ApiClient($serverHost, $serverPort);
        $this->client->onCommand('publish', function (string $commandAction, array $commandArgs) {
            $this->handleServerPublish($commandAction, $commandArgs);
        });

        $this->client->onConnected(function () {
            // 连接成功后再次向服务端注册
            foreach (array_keys($this->eventHandlerDic) as $eventName) {
                $this->sendSubscribe($eventName);
            }

            if (is_callable($this->onConnected)) {
                call_user_func($this->onConnected, $this);
            }
        });
    }

    /**
     * @return string 获取服务器连接地址
     */
    public function getServerAddress(): string
    {
        return $this->client->getServerAddress();
    }

    /**
     * @return string 获取服务器主机名
     */
    public function getServerHost(): string
    {
        return $this->client->getServerHost();
    }

    /**
     * @return int 获取服务器端口
     */
    public function getServerPort(): int
    {
        return $this->client->getServerPort();
    }

    /**
     * @return bool 当前是否已连接
     */
    public function isConnected(): bool
    {
        return $this->client->isConnected();
    }

    /**
     * 开始连接
     * @param bool $autoReconnect 断开后是否自动重连
     * @param int $reconnectDelay 自动重连前延时, 单位毫秒
     * @return void
     * @throws Throwable
     */
    public function connect(bool $autoReconnect = true, int $reconnectDelay = 2000): void
    {
        $this->client->setAutoReconnect($autoReconnect, $reconnectDelay);
        $this->client->connect();
    }

    /**
     * 关闭连接并停止自动重连
     * @return void
     * @throws Throwable
     */
    public function close(): void
    {
        $this->client->setAutoReconnect(false);
        $this->client->close();
    }

    /**
     * 设置开始连接时的回调
     * @param callable $handler
     * @return void
     */
    public function onConnecting(callable $handler): void
    {
        $this->client->onConnecting(fn() => $handler($this));
    }

    /**
     * 设置连接失败时的回调
     * @param callable $handler
     * @return void
     */
    public function onConnectFail(callable $handler): void
    {
        $this->client->onConnectFail(fn() => $handler($this));
    }

    /**
     * 设置连接成功时的回调
     * @param callable $handler
     * @return void
     */
    public function onConnected(callable $handler): void
    {
        $this->onConnected = $handler;
    }

    /**
     * 设置连接发生异常时的回调
     * @param callable $handler
     * @return void
     */
    public function onError(callable $handler): void
    {
        $this->client->onError(fn() => $handler($this));
    }

    /**
     * 设置连接已断开时的回调
     * @param callable $handler
     * @return void
     */
    public function onClosed(callable $handler): void
    {
        $this->client->onClosed(fn() => $handler($this));
    }

    /**
     * 设置发送订阅请求时的回调
     * @param callable $handler
     * @return void
     */
    public function onSubscribe(callable $handler): void
    {
        $this->onSubscribeHandler = $handler;
    }

    /**
     * 设置发送取消订阅请求时的回调
     * @param callable $handler
     * @return void
     */
    public function onUnSubscribe(callable $handler): void
    {
        $this->onUnSubscribeHandler = $handler;
    }

    /**
     * 设置发送发布请求返回时的回调
     * @param callable $handler
     * @return void
     */
    public function onPublishResponse(callable $handler): void
    {
        $this->onPublishResponseHandler = $handler;
    }

    /**
     * 设置发送订阅请求返回时的回调
     * @param callable $handler
     * @return void
     */
    public function onSubscribeResponse(callable $handler): void
    {
        $this->onSubscribeResponseHandler = $handler;
    }

    /**
     * 设置发送取消订阅请求返回时的回调
     * @param callable $handler
     * @return void
     */
    public function onUnSubscribeResponse(callable $handler): void
    {
        $this->onUnSubscribeResponseHandler = $handler;
    }

    /**
     * 发布事件
     * @param string $eventName
     * @param array|null $eventArgs
     * @return void
     * @throws Throwable
     */
    public function publish(string $eventName, ?array $eventArgs = null): void
    {
        $publishRequestArgs = ['eventName' => $eventName, 'eventArgs' => $eventArgs];
        $this->client->send('publish', $publishRequestArgs, function (ExecuteResult $responseResult) use ($eventName) {
            if (is_callable($this->onPublishResponseHandler)) call_user_func($this->onPublishResponseHandler, $eventName, $responseResult);
        });
    }

    /**
     * 同步直接发布事件 (普通网站后台用)
     * @param string $eventName
     * @param array|null $eventArgs
     * @return ExecuteResult 返回服务端对发布请求的响应结果
     */
    public function publishSync(string $eventName, ?array $eventArgs = null): ExecuteResult
    {
        $sendResult = $this->client->sendSync('publish', [
            'eventName' => $eventName,
            'eventArgs' => $eventArgs,
        ]);
        if (is_callable($this->onPublishResponseHandler)) {
            call_user_func($this->onPublishResponseHandler, $eventName, $sendResult);
        }
        return $sendResult;
    }

    /**
     * 同步直接发送事件, 并忽略服务端返回的响应 (普通网站后台用)
     * @param string $eventName
     * @param array|null $eventArgs
     * @return ExecuteResult 返回服务端对发布请求的响应结果
     */
    public function publishSyncNoResponse(string $eventName, ?array $eventArgs = null): ExecuteResult
    {
        return $this->client->sendSyncNoResponse('publish', [
            'eventName' => $eventName,
            'eventArgs' => $eventArgs,
        ]);
    }

    /**
     * 订阅事件
     * @param string $eventName
     * @param callable $callback
     * @return string
     * @throws Throwable
     */
    public function on(string $eventName, callable $callback): string
    {
        if (empty($eventName)) return "";
        if (!array_key_exists($eventName, $this->eventHandlerDic)) $this->eventHandlerDic[$eventName] = [];
        $eventHandlerUid                                     = "$eventName." . count($this->eventHandlerDic[$eventName]);
        $this->eventHandlerDic[$eventName][$eventHandlerUid] = $callback;
        if (is_callable($this->onSubscribeHandler)) call_user_func($this->onSubscribeHandler, $eventName, $eventHandlerUid);
        $this->sendSubscribe($eventName);
        return $eventHandlerUid;
    }

    /**
     * 取消事件订阅
     * @param string $eventName
     * @param string $eventHandlerUid
     * @return void
     * @throws Throwable
     */
    public function off(string $eventName, string $eventHandlerUid = '*'): void
    {
        if (!array_key_exists($eventName, $this->eventHandlerDic)) return;
        if (is_callable($this->onUnSubscribeHandler)) call_user_func($this->onUnSubscribeHandler, $eventName, $eventHandlerUid);
        if ($eventHandlerUid === '*') {
            unset($this->eventHandlerDic[$eventName]);
        } else {
            unset($this->eventHandlerDic[$eventName][$eventHandlerUid]);
        }

        // 如果该事件下没有处理器，通知服务端取消订阅
        if (!array_key_exists($eventName, $this->eventHandlerDic) || count($this->eventHandlerDic[$eventName]) === 0) {
            $this->sendUnSubscribe($eventName);
        }
    }

    /**
     * 取消所有事件订阅
     * @return void
     * @throws Throwable
     */
    public function offAll(): void
    {
        foreach ($this->eventHandlerDic as $eventName => $_) {
            $this->off($eventName);
        }
    }

    /**
     * 发送订阅指令
     * @param string $eventName
     * @return void
     * @throws Throwable
     */
    private function sendSubscribe(string $eventName): void
    {
        $this->client->send('subscribe', ['eventName' => $eventName], function (ExecuteResult $responseResult) use ($eventName) {
            if (is_callable($this->onSubscribeResponseHandler)) call_user_func($this->onSubscribeResponseHandler, $eventName, $responseResult);
            if (!$responseResult->result) {
                Timer::add(1, fn() => $this->sendSubscribe($eventName), null, false);
            }
        });
    }

    /**
     * 发送取消订阅指令
     * @param string $eventName
     * @return void
     * @throws Throwable
     */
    private function sendUnSubscribe(string $eventName): void
    {
        $this->client->send('unsubscribe', ['eventName' => $eventName], function (ExecuteResult $responseResult) use ($eventName) {
            if (is_callable($this->onUnSubscribeResponseHandler)) call_user_func($this->onUnSubscribeResponseHandler, $eventName, $responseResult);
            if (!$responseResult->result) {
                Timer::add(1, fn() => $this->sendUnSubscribe($eventName), null, false);
            }
        });
    }

    /**
     * 处理服务端下发的publish命令
     * @param string $commandAction
     * @param array $commandArgs
     * @throws Throwable
     */
    private function handleServerPublish(string $commandAction, array $commandArgs): void
    {
        assert($commandAction === 'publish');
        $eventName = $commandArgs['eventName'];
        if (!array_key_exists($eventName, $this->eventHandlerDic)) {
            $this->sendUnSubscribe($eventName);
        } else {
            $eventArgs = $commandArgs['eventArgs'];
            foreach ($this->eventHandlerDic[$eventName] as $callback) {
                $callback($eventName, $eventArgs);
            }
        }
    }
}